package main

/*
界面包含：
一个输入框：输入命令
一个列表：显示历史记录
输入框内容变化时，用下拉列表实时显示可能的选项
*/

import (
	"encoding/json"
	"fmt"
	"image/color"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"sync"

	"fyne.io/fyne/v2/dialog"

	//	"fyne.io/fyne/v2/layout"

	"database/sql"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/data/binding"
	"fyne.io/fyne/v2/storage"
	"fyne.io/fyne/v2/widget"

	_ "github.com/mattn/go-sqlite3"
)

var downScroll *container.Scroll
var historyScroll *container.Scroll
var progEntry *widget.Entry
var paramEntry *widget.Entry
var mainWin fyne.Window
var prog binding.String
var folderDir string
var fileDir string

type myConfig struct {
	FileDir   string `json:filedir`
	FolderDir string `json:folderdir`
}

func init() {
	name := getConfigPath()
	var cfg myConfig
	data, err := ioutil.ReadFile(name)
	if err == nil && len(data) > 0 {
		err = json.Unmarshal(data, &cfg)
		if err != nil {
			fmt.Println(err.Error())
			return
		} else {
			fileDir = cfg.FileDir
			folderDir = cfg.FolderDir
		}
	} else {
		fmt.Println(err.Error())
	}
}

func saveConfig() {
	name := getConfigPath()
	fmt.Println("save config", name, fileDir, folderDir)
	var cfg = myConfig{FileDir: fileDir, FolderDir: folderDir}
	data, err := json.Marshal(&cfg)
	if err == nil {
		err = ioutil.WriteFile(name, data, 0644)
		if err != nil {
			fmt.Println(err.Error())
		}

	} else {
		fmt.Println(err.Error())
	}

}

func main() {
	defer saveConfig()
	app := app.New()
	app.Settings().SetTheme(GetMyTheme())
	//fmt.Println(r.Name())
	app.Lifecycle().SetOnExitedForeground(func() {
		app.Quit()
	})

	w := app.NewWindow("Quick-Run")

	blank := canvas.NewRectangle(color.Black)

	progEntry = makeEntry()
	downScroll = container.NewVScroll(blank)
	downScroll.SetMinSize(fyne.NewSize(640, 480))
	historyScroll = container.NewVScroll(blank)
	btEntry := widget.NewButton("Run", func() {
		progEntry.TypedKey(&fyne.KeyEvent{Name: fyne.KeyEnter, Physical: fyne.HardwareKey{ScanCode: 0x1c00}})
	})

	btClear := widget.NewButton("X", func() {
		progEntry.SetText("")
	})
	progEntry.SetPlaceHolder("2种用法：1、输入完整路径;2、输入PATH中的程序名。")
	mEntry := container.NewGridWrap(fyne.NewSize(550, 38), progEntry)

	progEntry.MultiLine = false
	//未完成

	paramEntry = widget.NewEntry()
	paramEntry.PlaceHolder = "此处输入运行参数，右侧按钮可以插入文件/目录路径"
	btFile := widget.NewButton("File", func() {
		filedialog1 := dialog.NewFileOpen(func(r fyne.URIReadCloser, e error) {
			if e != nil || r == nil {
				return
			}
			p := r.URI().Path()
			fileDir = filepath.Dir(p)
			paramEntry.SetText(paramEntry.Text + encSpace(p))
		}, w)
		if len(fileDir) > 0 {
			uri1 := storage.NewFileURI(fileDir)
			luri1, _ := storage.ListerForURI(uri1)
			filedialog1.SetLocation(luri1)
		}

		filedialog1.Show()
		// dialog.ShowFileOpen(func(r fyne.URIReadCloser, e error) {
		// 	if e != nil || r == nil {
		// 		return
		// 	}
		// 	p := r.URI().Path()
		// 	paramEntry.SetText(paramEntry.Text + encSpace(p))
		// }, w)
	})
	btDir := widget.NewButton("Dir", func() {
		folderdialog1 := dialog.NewFolderOpen(func(r fyne.ListableURI, e error) {
			if e != nil || r == nil {
				return
			}
			p := r.Path()
			folderDir = p
			paramEntry.SetText(paramEntry.Text + encSpace(p))
		}, w)
		if len(folderDir) > 0 {
			uri1 := storage.NewFileURI(folderDir)
			luri1, _ := storage.ListerForURI(uri1)
			folderdialog1.SetLocation(luri1)
		}

		folderdialog1.Show()
		// dialog.ShowFolderOpen(func(r fyne.ListableURI, e error) {
		// 	if e != nil || r == nil {
		// 		return
		// 	}
		// 	p := r.Path()
		// 	paramEntry.SetText(paramEntry.Text + encSpace(p))
		// }, w)
	})
	mParamEntry := container.NewGridWrap(fyne.NewSize(550, 38), paramEntry)
	boxParam := container.NewHBox(mParamEntry, btFile, btDir)
	//-end

	btClear.Alignment = widget.ButtonAlignTrailing
	btEntry.Alignment = widget.ButtonAlignTrailing

	boxHeader := container.NewHBox(mEntry, btClear, btEntry)

	box1 := container.NewVBox(boxHeader, boxParam, downScroll, historyScroll)

	mainWin = w
	w.SetContent(box1)
	w.Resize(fyne.NewSize(640, 480))

	w.SetOnClosed(func() {
		app.Quit()
	})

	downScroll.Hide()
	historyScroll.SetMinSize(fyne.NewSize(640, 480))
	showHistory()
	w.ShowAndRun()
}

func encSpace(s string) string {
	return strings.Replace(s, " ", `\s`, -1)
}

func decSpace(s string) string {
	return strings.Replace(s, `\s`, " ", -1)
}

func ErrPanic(err error) {
	if err != nil {
		panic(err)
	}
}

func listDir(p string) []string {
	ret := []string{}
	f, err := os.Open(p)
	if err != nil {
		return nil
	}
	defer f.Close()
	items, err := f.ReadDir(-1)
	if err != nil {
		return nil
	}
	for _, item := range items {
		fullpath := filepath.Join(p, item.Name())
		if item.IsDir() {
			fullpath = fullpath + "/"
		} else if !fileIsExec(fullpath) {
			continue
		}
		ret = append(ret, fullpath)
	}
	return ret
}

var maybeList *widget.List
var maybeData binding.ExternalStringList

func makeMaybeList(items []string) {
	data := binding.BindStringList(&items)
	maybeList = widget.NewListWithData(data, func() fyne.CanvasObject {
		return &widget.Label{Text: "Maybe"}
	},
		func(i binding.DataItem, o fyne.CanvasObject) {
			o.(*widget.Label).Bind(i.(binding.String))
		},
	)
	maybeList.OnSelected = func(id widget.ListItemID) {
		//fmt.Println(items[id])
		val, err := maybeData.GetValue(id)
		if err != nil {
			return
		}
		progEntry.SetText(val)
		if !strings.HasSuffix(val, "/") {
			downScroll.Hide()
			historyScroll.Show()
		}
		mainWin.Canvas().Focus(progEntry)
	}
}

var onceMaybeList sync.Once

func updateMaybeList(items []string) {
	onceMaybeList.Do(func() {
		v := []string{}
		maybeData = binding.BindStringList(&v)
		maybeList = widget.NewListWithData(maybeData, func() fyne.CanvasObject {
			return &widget.Label{Text: "Maybe"}
		},
			func(i binding.DataItem, o fyne.CanvasObject) {
				o.(*widget.Label).Bind(i.(binding.String))
			},
		)
		maybeList.OnSelected = func(id widget.ListItemID) {
			//fmt.Println(items[id])
			val, err := maybeData.GetValue(id)
			if err != nil {
				return
			}
			progEntry.SetText(val)
			if !strings.HasSuffix(val, "/") {
				downScroll.Hide()
				showHistory()
			}
			mainWin.Canvas().Focus(progEntry)
			maybeList.Unselect(id)
		}
	})
	maybeData.Set(items)
}

func makeEntry() *widget.Entry {
	var ret = widget.NewEntry()
	prog = binding.NewString()
	ret.Bind(prog)
	prog.AddListener(binding.NewDataListener(func() {
		s, err := prog.Get()
		if err != nil {
			return
		}
		info, err := os.Stat(s)
		if err == nil {
			if info.IsDir() {
				items := listDir(s)
				showSearchResult(items)
				historyScroll.Hide()
			} else {
				downScroll.Hide()
				historyScroll.Show()
			}
		} else {
			matchPath(s)
		}
	}))

	ret.OnSubmitted = func(s string) {
		if execProg(s) {
			showHistory()
		}
	}

	return ret
}

func execProg(s string) bool {
	info, err := os.Stat(s)
	if err != nil {
		return false
	}
	if info.IsDir() {
		return false
	}

	param := paramEntry.Text
	pv := []string{}
	if len(param) > 0 {
		for _, v := range strings.Split(param, " ") {
			if len(v) > 0 {
				pv = append(pv, decSpace(v))
			}
		}

	}

	if isExec(info.Mode()) {
		go func() {
			cmd := exec.Command(s, pv...)
			cmd.Run()
			cmd.Wait()
		}()
		saveHistory(s)
	}
	return true
}

func matchInPath(p, s string) []string {
	res := []string{}
	d, err := os.Open(p)
	if err != nil {
		return res
	}
	names, err := d.Readdirnames(-1)
	if err != nil {
		return res
	}
	for _, v := range names {
		if strings.HasPrefix(v, s) {
			res = append(res, filepath.Join(p, v))
		}
	}
	return res
}

func matchPath(p string) {
	if len(p) == 0 {
		downScroll.Hide()
		historyScroll.Show()
		return
	}
	if !strings.HasPrefix(p, "/") {
		paths := os.Getenv("PATH")
		pathv := strings.Split(paths, ":")
		items := []string{}
		for _, v := range pathv {
			pfull := filepath.Join(v, p)

			info, err := os.Stat(pfull)
			if err != nil {
				res := matchInPath(v, p)
				items = append(items, res...)
				continue
			}
			if isExec(info.Mode()) {
				items = append(items, pfull)
				//fmt.Println(pfull)
			}
		}
		showSearchResult(items)

		return
	}
	d := filepath.Dir(p)
	files := listDir(d)
	items := []string{}
	for _, v := range files {
		if strings.HasPrefix(v, p) {
			items = append(items, v)
		}
	}
	showSearchResult(items)
}

func showSearchResult(items []string) {
	if len(items) == 0 {
		downScroll.Hide()
		historyScroll.Show()
		return
	}
	sort.Strings(items)
	updateMaybeList(items)
	downScroll.Content = maybeList
	downScroll.Refresh()
	downScroll.Show()
	mainWin.Show()
	downScroll.Resize(fyne.NewSize(640, 480))
	historyScroll.Hide()
}
func isExec(mode os.FileMode) bool {
	return mode&0111 != 0
}

func fileIsExec(p string) bool {
	info, err := os.Stat(p)
	if err != nil {
		return false
	}
	if info.IsDir() {
		return false
	}
	mode := info.Mode()
	return mode&0111 != 0
}

func getConfigPath() string {
	home1, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	name := "quick-run"
	os.MkdirAll(filepath.Join(home1, ".config", name), 0700)
	return filepath.Join(home1, ".config", name, "config.json")
}

func getDbPath() string {
	home1, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	name := "quick-run"
	os.MkdirAll(filepath.Join(home1, ".config", name), 0700)
	return filepath.Join(home1, ".config", name, "history.db")
}

var onceDb sync.Once

func getConn() *sql.DB {
	filename := getDbPath()
	conn, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_mutex=full&_busy_timeout=9999999", filename))
	if err != nil {
		return nil
	}

	onceDb.Do(func() {
		_, err := conn.Exec("CREATE TABLE IF NOT EXISTS history(access_time timestamp, pathname text unique);")
		if err != nil {
			panic(err)
		}
	})
	return conn
}

func saveHistory(cmd string) {
	conn := getConn()
	if conn == nil {
		return
	}
	defer conn.Close()
	_, err := conn.Exec("insert into history values (datetime('now'), ?);", cmd)
	if err != nil {
		//fmt.Println(err.Error(), "insert")
		_, err = conn.Exec("update history set access_time=datetime('now') where pathname=?;", cmd)
		if err != nil {
			//fmt.Println(err.Error(), "update")
		} else {
			//fmt.Println("save", cmd)
		}

	}
}

var historyList *widget.List
var historyData binding.ExternalStringList
var onceHistory sync.Once

func createHistoryList() {
	data := []string{}
	historyData = binding.BindStringList(&data)
	historyList = widget.NewListWithData(historyData,
		func() fyne.CanvasObject {
			res := &widget.Label{Text: "History"}
			return res
		},
		func(i binding.DataItem, o fyne.CanvasObject) {
			o.(*widget.Label).Bind(i.(binding.String))
		})
	historyList.OnSelected = func(id widget.ListItemID) {
		v, err := historyData.GetValue(id)
		if err != nil {
			return
		}
		progEntry.SetText(v)
		historyList.Unselect(id)
		mainWin.Canvas().Focus(progEntry)
	}
	historyScroll.Content = historyList
	historyScroll.Refresh()
}

func showHistory() {
	onceHistory.Do(func() {
		createHistoryList()
	})
	conn := getConn()
	if conn == nil {
		return
	}
	defer conn.Close()
	res, err := conn.Query("select pathname from history order by access_time desc;")
	if err != nil {
		return
	}
	var data = []string{}
	for {
		if !res.Next() {
			break
		}
		var pathname string
		res.Scan(&pathname)
		data = append(data, pathname)
		//fmt.Println("record:", pathname)
	}
	historyData.Set(data)
	historyScroll.Show()
	mainWin.Show()
}
